<?php
/**
 * blXsltSql
 *
 * @version: 2.2
 * @date: 2008-12-04
 * @author: Vladimir Romanovich <ibnteo@gmail.com>
 * @license: GPL2
 */
class blXsltSql
{
    protected $bl_xslt;

    // SQL код запроса    
    protected $sql;

    // параметры SQL запроса    
    protected $params;

    // настройки подключения к БД    
    protected $setting_db = array();

    // элемент в который выводятся данные
    protected $result;

    // размер страницы
    protected $page_size = 10;

    // номер страницы
    protected $page_number = 0;

    // имя элемента списка
    protected $item_name = 'item';

    // список полей, которые нужно положить в атрибуты
    protected $elements_attribute = array();

    // список полей, которые нужно преобразовать в xml
    protected $elements_xml = array();

    // соединение с БД
    protected $dbh;

    // запрос к БД
    protected $query;

    // использовать ли механизм транзакции при выполнения запроса
    protected $is_transaction = FALSE;

    // количество возвращенных записей в запросе
    protected $row_count = 0;

    /**
     * Конструктор
     */
    public function __construct($bl_xslt)
    {
        $this->bl_xslt = $bl_xslt;
    }

    /**
     * Выполнить SQL-запрос
     */
    public function xbl_sql()
    {
        $this->bl_xslt->set_document_pi();

        // получить настройки подключения к БД
        $this->get_setting();

        // сформировать узел для вывода данных
        $this->result = $this->bl_xslt->doc->createElement($this->bl_xslt->element_pi_next->nodeName);

        // получить текст SQL-запроса
        $this->sql = $this->get_sql();

        // указать SQL-запрос
        $this->result->setAttribute('sql', $this->sql);

        // получить различные данные
        $this->get_attributes();

        // сформировать список параметров SQL-запроса
        $this->xml_param();

        // выполнить SQL-запрос с параметрами
        $this->connect();

        // будет ли запрос исполняться в режиме транзакции
        $this->is_transaction = ($this->bl_xslt->element_pi_next->getAttribute('transaction') === 'yes');

        if ($this->is_transaction)
        {
            // начать транзакцию
            $this->dbh->beginTransaction();
        }

        // выполнение запроса
        $this->query = $this->dbh->prepare($this->sql);
        if ( ! $this->query->execute($this->params))
        {
            // обработать ошибку
            $this->show_error();
        } else {
            if ($this->is_transaction)
            {
                // выполнить транзакцию
                $this->dbh->commit();
            }
        }

        // получить ID последней добавленной записи
        $this->get_last_id();

        // получить количество записей по SQL-запросу
        $this->row_count = $this->query->rowCount();

        // пропустить записи первых страниц
        $record_skip = $this->skip_pages();

        // выбрать данные текущей страницы
        $record_number = $this->get_items();

        // посчитать количество записей, если не работает rowCount() (например в sqlite)
        if ( ! $this->row_count)
        {
            $this->row_count = $record_number + $record_skip;
            while ($row = $this->query->fetch())
            {
                $this->row_count ++;
            }
        }

        // указать количество записей запроса
        $this->result->setAttribute('count', $this->row_count);

        // сформировать список страниц
        if ($this->page_size and $this->row_count)
        {
            // страницы предыдущая и следующая
            $this->result->appendChild($this->pagination_records());

            // список страниц
            $this->result->appendChild($this->pagination_pages());
        }

        // удалить узел с запросом
        $this->bl_xslt->element_pi_next->parentNode->removeChild($this->bl_xslt->element_pi_next);

        // вставить результаты запроса в документ
        $this->bl_xslt->element_pi->parentNode->insertBefore($this->result, $this->bl_xslt->element_pi);
    }

    /**
     * Получить текст SQL-запроса
     */
    protected function get_sql()
    {
        $xpath = new DomXpath($this->bl_xslt->doc);
        $sql = '';
        foreach ($xpath->query('text()', $this->bl_xslt->element_pi_next) as $text)
        {
            $sql .= $text->nodeValue;
        }
        return $sql;
    }

    /**
     * Получить различные данные
     */
    protected function get_attributes()
    {
        $attribute_node = $this->bl_xslt->element_pi_next;

        // получить количество записей на странице
        $this->page_size = $attribute_node->getAttribute('page-size');
        if ($this->page_size)
        {
            $this->page_number = $attribute_node->getAttribute('page-number') - 1;
            if (( ! $this->page_number) or $this->page_number < 0) $this->page_number = 0;
        }

        // имя элемента списка
        $item_name = $attribute_node->getAttribute('item-name');
        if ($item_name)
        {
            $this->item_name = $item_name;
        }

        // массив полей, которые нужно положить в атрибуты
        $this->elements_attribute = split(' ', $attribute_node->getAttribute('elements-attribute'));

        // массив полей, которые нужно преобразовать в xml
        $this->elements_xml = split(' ', $attribute_node->getAttribute('elements-xml'));
    }

    /**
     * Создать соединение с БД
     */
    protected function connect()
    {
        $this->dbh = new PDO($this->setting_db['dsn'], $this->setting_db['user'], $this->setting_db['passwd']);
        $this->dbh->exec('set character set utf8');
        $this->dbh->exec('set names utf8');
    }

    /**
     * Обработать ошибку выполения запроса
     */
    protected function show_error()
    {
        $error_info = $this->query->errorInfo();
        if ($this->is_transaction)
        {
            // откатить трансакцию
            $this->dbh->rollBack();
        }
        if ($this->bl_xslt->element_pi_next->getAttribute('exception') === 'no')
        {
            // сообщение об ошибке в результат запроса
            $this->result->setAttribute('error', $error_info[2]);
        }
        else
        {
            // прервать выполнение программы
            throw new Exception($error_info[2]);
        }
    }

    /**
     * Получить ID последней добавленной записи
     */
    protected function get_last_id()
    {
        $last_id = $this->dbh->lastInsertId();
        if ($last_id)
        {
            $this->result->setAttribute('last-id', $last_id);
        }
    }

    /**
     * Пропустить начальные страницы
     */
    protected function skip_pages()
    {
        $record_skip = 0;
        if ($this->page_size)
        {
            for ($page_index = 0; $page_index < ($this->page_size * $this->page_number); $page_index ++)
            {
                if ( ! $this->query->fetch())
                {
                    break;
                }
                $record_skip ++;
            }
        }
        return $record_skip;
    }

    /**
     * Получить выбираемые записи
     */
    protected function get_items()
    {
        $record_number = 0;
        while ($row = $this->query->fetch())
        {
            $item = $this->bl_xslt->doc->createElement($this->item_name);
            foreach ($row as $name => $field)
            {
                if (is_string($name) and ( ! is_null($field)))
                {
                    $name = strtolower($name);
                    if (in_array($name, $this->elements_attribute))
                    {
                        $item->setAttribute($name, $field);
                    }
                    elseif (in_array($name, $this->elements_xml))
                    {
                        $doc_node = new DomDocument();
                        $doc_node->loadXml('<'.$name.'>'.$field.'</'.$name.'>');
                        $item->appendChild($item->ownerDocument->importNode($doc_node->documentElement, TRUE));
                    }
                    else
                    {
                        $item_field = $this->bl_xslt->doc->createElement($name);
                        $item_value = $this->bl_xslt->doc->createTextNode($field);
                        $item_field->appendChild($item_value);
                        $item->appendChild($item_field);
                    }
                }
            }
            $this->result->appendChild($item);
            $record_number ++;
            if ($this->page_size and $this->page_size <= $record_number)
            {
                break;
            }
        }
        return $return_number;
    }

    /**
     * получить настройки подключения к БД
     */
    protected function get_setting()
    {
        $db_name = $this->bl_xslt->attributes_pi->getAttribute('db');
        if ( ! isset($this->bl_xslt->config['setting_db']))
        {
            $this->bl_xslt->config['setting_db'] = array();
        }
        $setting_db = $this->bl_xslt->config['setting_db'];
        if ( ! isset($setting_db[$db_name]))
        {
            $doc_setting = new DomDocument();
            $doc_setting->load('setting.xbl');
            $xpath = new DomXpath($doc_setting);
            $database = $xpath->query('/setting/database' . ($db_name ? '[@name="' . $db_name . '"]' : ''));
            $db0 = $database->item(0);
            $setting_db[$db_name] = array();
            $setting_db[$db_name]['dsn'] = $db0->getAttribute('dsn');
            $setting_db[$db_name]['user'] = $db0->getAttribute('user');
            $setting_db[$db_name]['passwd'] = $db0->getAttribute('passwd');
        }
        $this->setting_db = $setting_db[$db_name];
    }

    /**
     * Страницы предыдущая и следующая
     */
    protected function pagination_records()
    {
        $records = $this->bl_xslt->doc->createElement('records');
        $records->setAttribute('count', $this->row_count);
        if ($this->page_number > 0)
        {
            $previous = $this->bl_xslt->doc->createElement('previous');
            $page = $this->page_number;
            if ($page < 1) $page = 1;
            if ($page > ceil($this->row_count / $this->page_size)) $page = ceil($this->row_count / $this->page_size);
            $previous->setAttribute('page', $page);
            $start = ($this->page_number - 1) * $this->page_size + 1;
            if ($start < 1) $start = 1;
            if ($start > $row_count) $start = (ceil($this->row_count / $this->page_size) - 1) * $this->page_size + 1;
            $previous->setAttribute('start', $start);
            $end = $this->page_number * $this->page_size;
            if ($end > $this->row_count) $end = $this->row_count;
            if ($start !== $end) $previous->setAttribute('end', $end);
            $records->appendChild($previous);
        }
        if ($this->page_number + 1 < ceil($this->row_count / $this->page_size))
        {
            $next = $this->bl_xslt->doc->createElement('next');
            $next->setAttribute('page', $this->page_number + 2);
            $start = ($this->page_number + 1) * $this->page_size + 1;
            if ($start < 1) $start = 1;
            if ($start > $this->row_count) $start = $this->row_count;
            $next->setAttribute('start', $start);
            $end = ($this->page_number + 2) * $this->page_size;
            if ($end > $this->row_count) $end = $this->row_count;
            if ($start !== $end) $next->setAttribute('end', $end);
            $records->appendChild($next);
        }
        return $records;
    }

    /**
     * Список страниц
     */
    protected function pagination_pages()
    {
        $pages = $this->bl_xslt->doc->createElement('pages');
        $pages->setAttribute('count', ceil($this->row_count / $this->page_size));
        $pages->setAttribute('current', $this->page_number + 1);
        $pages->setAttribute('size', $page_size);
        for ($page_index = 0; $page_index < ceil($this->row_count / $this->page_size); $page_index ++)
        {
            $page = $this->bl_xslt->doc->createElement('page', $page_index + 1);
            if ($page_index === $this->page_number) $page->setAttribute('current', 'true');
            $pages->appendChild($page);
        }
        return $pages;
    }

    /**
     * Преобразовать параметры в массив
     */
    public function xml_param()
    {
        $params = array();
        foreach ($this->bl_xslt->element_pi_next->childNodes as $param_item)
        {
            if ($param_item->nodeType === XMLReader::ELEMENT)
            {
                $param_value = '';
                $param_xpath = $param_item->getAttribute('xpath');
                if ($param_xpath)
                {
                    // подставить значение из xpath
                    $xpath = new DomXpath($this->bl_xslt->doc_in);
                    $param = $xpath->query($param_xpath);
                    if ($param->length > 0)
                    {
                        $param_value = $param->item(0)->nodeValue;
                    }
                }
                else
                {
                    // подставить значение из value
                    $param_value = $param_item->getAttribute('value');
                }
                $params[] = $param_value;
            }
        }
        $this->params = $params;
    }

} // End blXsltSql
